using System;
using System.Collections.Generic;
using UnityEngine.Networking;
using UnityEngine.ResourceManagement.AsyncOperations;
using UnityEngine.ResourceManagement.Exceptions;
using UnityEngine.ResourceManagement.ResourceLocations;
using UnityEngine.ResourceManagement.ResourceProviders;
using UnityEngine.ResourceManagement.Util;
using UnityEngine.SceneManagement;

namespace UnityEngine.ResourceManagement
{
	/// <summary>
	/// Entry point for ResourceManager API
	/// </summary>
	public class ResourceManager : IDisposable
	{
		/// <summary>
		/// Options for event types that will be sent by the ResourceManager
		/// </summary>
		public enum DiagnosticEventType
		{
			/// <summary>
			/// Use to indicate that an operation failed.
			/// </summary>
			AsyncOperationFail,

			/// <summary>
			/// Use to indicate that an operation was created.
			/// </summary>
			AsyncOperationCreate,

			/// <summary>
			/// Use to indicate the percentage of completion for an operation was updated.
			/// </summary>
			AsyncOperationPercentComplete,

			/// <summary>
			/// Use to indicate that an operation has completed.
			/// </summary>
			AsyncOperationComplete,

			/// <summary>
			/// Use to indicate that the reference count of an operation was modified.
			/// </summary>
			AsyncOperationReferenceCount,

			/// <summary>
			/// Use to indicate that an operation was destroyed.
			/// </summary>
			AsyncOperationDestroy,
		}

		internal bool postProfilerEvents = false;

		/// <summary>
		/// Container for information associated with a Diagnostics event.
		/// </summary>
		public struct DiagnosticEventContext
		{
			/// <summary>
			/// Operation handle for the event.
			/// </summary>
			public AsyncOperationHandle OperationHandle { get; }

			/// <summary>
			/// The type of diagnostic event.
			/// </summary>
			public DiagnosticEventType Type { get; }

			/// <summary>
			/// The value for this event.
			/// </summary>
			public int EventValue { get; }

			/// <summary>
			/// The IResourceLocation being provided by the operation triggering this event.
			/// This value is null if the event is not while providing a resource.
			/// </summary>
			public IResourceLocation Location { get; }

			/// <summary>
			/// Addition data included with this event.
			/// </summary>
			public object Context { get; }

			/// <summary>
			/// Any error that occurred.
			/// </summary>
			public string Error { get; }

			/// <summary>
			/// Construct a new DiagnosticEventContext.
			/// </summary>
			/// <param name="op">Operation handle for the event.</param>
			/// <param name="type">The type of diagnostic event.</param>
			/// <param name="eventValue">The value for this event.</param>
			/// <param name="error">Any error that occurred.</param>
			/// <param name="context">Additional context data.</param>
			public DiagnosticEventContext(AsyncOperationHandle op, DiagnosticEventType type, int eventValue = 0, string error = null, object context = null)
			{
				OperationHandle = op;
				Type = type;
				EventValue = eventValue;
				Location = op.m_InternalOp != null && op.m_InternalOp is IGenericProviderOperation gen
					? gen.Location
					: null;
				Error = error;
				Context = context;
			}
		}

		/// <summary>
		/// Global exception handler.  This will be called whenever an IAsyncOperation.OperationException is set to a non-null value.
		/// </summary>
		/// <example>
		/// <code source="../../Tests/Editor/DocExampleCode/AddExceptionHandler.cs" region="doc_AddExceptionHandler" title="Adding a global exception handler"/>
		/// </example>
		public static Action<AsyncOperationHandle, Exception> ExceptionHandler { get; set; }

		/// <summary>
		/// Functor to transform internal ids before being used by the providers.
		/// </summary>
		/// <remarks>
		/// Used to assign a function to the [ResourceManager](xref:UnityEngine.ResourceManagement.ResourceManager)  that replaces location identifiers used at runtime.
		/// This is useful when you want to load assets from a different location than the one specified in the content catalog,
		/// for example downloading a remote AssetBundle from a different URL.
		///
		/// Assigning this value through the <see cref="Addressables"/> object will set the value on the <see cref="ResourceManager"/>.
		/// 
		/// The example below instantiates a GameObject from a local AssetBundle. The location identifier of the bundle is replaced with a file URI, and so the bundle is loaded via UnityWebRequest. 
		/// <code source="../../Tests/Editor/DocExampleCode/ScriptReference/UsingInternalIdTransformFunc.cs" region="SAMPLE"/>/// 
		/// </remarks>
		/// <value>A function taking an [IResourceLocation](xref:UnityEngine.ResourceManagement.ResourceLocations.IResourceLocation) and returning a transformed string location.</value>
		/// <seealso href="xref:addressables-api-transform-internal-id">Transforming resource URLs</seealso>
		public Func<IResourceLocation, string> InternalIdTransformFunc { get; set; }

		/// <summary>
		/// Checks for an internal id transform function and uses it to modify the internal id value.
		/// </summary>
		/// <param name="location">The location to transform the internal id of.</param>
		/// <returns>If a transform func is set, use it to pull the local id; otherwise, the InternalId property of the location is used.</returns>
		public string TransformInternalId(IResourceLocation location)
		{
			return InternalIdTransformFunc == null ? location.InternalId : InternalIdTransformFunc(location);
		}

		/// <summary>
		/// Delegate that can be used to override the web request options before being sent.
		/// </summary>
		/// <remarks>
		/// You can assign a function to the <see cref="Addressables"/> object's WebRequestOverride property to individually modify the <see cref="UnityWebRequest"/> which is used to download files.
		///
		/// This can be used to add custom request headers or query strings.
		///
		/// This affects all downloads through Addressables including catalog files and asset bundles.
		///
		/// Assigning this value through the <see cref="Addressables"/> object will set the value on the <see cref="ResourceManager"/>.
		/// 
		/// For example you could add an Authorization header to authenticate with Cloud Content Delivery's private buckets.
		/// <code source="../../Tests/Editor/DocExampleCode/ScriptReference/UsingWebRequestOverride.cs" region="SAMPLE" />
		/// </remarks>
		/// <seealso href="xref:addressables-api-transform-internal-id#webrequest-override">Transforming resource URLs</seealso>
		public Action<UnityWebRequest> WebRequestOverride { get; set; }

		internal bool CallbackHooksEnabled = true; // tests might need to disable the callback hooks to manually pump updating

		ListWithEvents<IResourceProvider> m_ResourceProviders = new ListWithEvents<IResourceProvider>();
		IAllocationStrategy m_allocator;

		// list of all the providers in s_ResourceProviders that implement IUpdateReceiver
		internal ListWithEvents<IUpdateReceiver> m_UpdateReceivers = new ListWithEvents<IUpdateReceiver>();

		List<IUpdateReceiver> m_UpdateReceiversToRemove = null;

		//this prevents removing receivers during iteration
		bool m_UpdatingReceivers = false;

		//this prevents re-entrance into the Update method, which can cause stack overflow and infinite loops
		bool m_InsideUpdateMethod = false;

		internal int OperationCacheCount
		{
			get { return m_AssetOperationCache.Count; }
		}

		internal int InstanceOperationCount
		{
			get { return m_TrackedInstanceOperations.Count; }
		}

		//cache of type + providerId to IResourceProviders for faster lookup
		internal Dictionary<int, IResourceProvider> m_providerMap = new Dictionary<int, IResourceProvider>();
		Dictionary<IOperationCacheKey, IAsyncOperation> m_AssetOperationCache = new Dictionary<IOperationCacheKey, IAsyncOperation>();
		HashSet<InstanceOperation> m_TrackedInstanceOperations = new HashSet<InstanceOperation>();
		internal DelegateList<float> m_UpdateCallbacks = DelegateList<float>.CreateWithGlobalCache();
		List<IAsyncOperation> m_DeferredCompleteCallbacks = new List<IAsyncOperation>();
        HashSet<IResourceProvider> m_AssetBundleProviders = new HashSet<IResourceProvider>();

		bool m_InsideExecuteDeferredCallbacksMethod = false;
		List<DeferredCallbackRegisterRequest> m_DeferredCallbacksToRegister = null;

		private struct DeferredCallbackRegisterRequest
		{
			internal IAsyncOperation operation;
			internal bool incrementRefCount;
		}

		Action<AsyncOperationHandle, DiagnosticEventType, int, object> m_obsoleteDiagnosticsHandler; // For use in working with Obsolete RegisterDiagnosticCallback method.
		Action<DiagnosticEventContext> m_diagnosticsHandler;
		Action<IAsyncOperation> m_ReleaseOpNonCached;
		Action<IAsyncOperation> m_ReleaseOpCached;
		Action<IAsyncOperation> m_ReleaseInstanceOp;
		static int s_GroupOperationTypeHash = typeof(GroupOperation).GetHashCode();
		static int s_InstanceOperationTypeHash = typeof(InstanceOperation).GetHashCode();

		/// <summary>
		/// Add an update receiver.
		/// </summary>
		/// <param name="receiver">The object to add. The Update method will be called until the object is removed. </param>
		public void AddUpdateReceiver(IUpdateReceiver receiver)
		{
			if (receiver == null)
				return;
			m_UpdateReceivers.Add(receiver);
		}

		/// <summary>
		/// Remove update receiver.
		/// </summary>
		/// <param name="receiver">The object to remove.</param>
		public void RemoveUpdateReceiver(IUpdateReceiver receiver)
		{
			if (receiver == null)
				return;

			if (m_UpdatingReceivers)
			{
				if (m_UpdateReceiversToRemove == null)
					m_UpdateReceiversToRemove = new List<IUpdateReceiver>();
				m_UpdateReceiversToRemove.Add(receiver);
			}
			else
			{
				m_UpdateReceivers.Remove(receiver);
			}
		}

		/// <summary>
		/// The allocation strategy object.
		/// </summary>
		public IAllocationStrategy Allocator
		{
			get { return m_allocator; }
			set { m_allocator = value; }
		}

		/// <summary>
		/// Gets the list of configured <see cref="IResourceProvider"/> objects. Resource Providers handle load and release operations for <see cref="IResourceLocation"/> objects.
		/// </summary>
		/// <value>The resource providers list.</value>
		public IList<IResourceProvider> ResourceProviders
		{
			get { return m_ResourceProviders; }
		}

		/// <summary>
		/// The CertificateHandler instance object.
		/// </summary>
		public CertificateHandler CertificateHandlerInstance { get; set; }

		/// <summary>
		/// Constructor for the resource manager.
		/// </summary>
		/// <param name="alloc">The allocation strategy to use.</param>
		public ResourceManager(IAllocationStrategy alloc = null)
		{
			m_ReleaseOpNonCached = OnOperationDestroyNonCached;
			m_ReleaseOpCached = OnOperationDestroyCached;
			m_ReleaseInstanceOp = OnInstanceOperationDestroy;
			m_allocator = alloc == null ? new LRUCacheAllocationStrategy(1000, 1000, 100, 10) : alloc;
			m_ResourceProviders.OnElementAdded += OnObjectAdded;
			m_ResourceProviders.OnElementRemoved += OnObjectRemoved;
			m_UpdateReceivers.OnElementAdded += x => RegisterForCallbacks();
#if ENABLE_ADDRESSABLE_PROFILER
            Profiling.ProfilerRuntime.Initialise();
#endif
		}

		private void OnObjectAdded(object obj)
		{
			IUpdateReceiver updateReceiver = obj as IUpdateReceiver;
			if (updateReceiver != null)
				AddUpdateReceiver(updateReceiver);
		}

		private void OnObjectRemoved(object obj)
		{
			IUpdateReceiver updateReceiver = obj as IUpdateReceiver;
			if (updateReceiver != null)
				RemoveUpdateReceiver(updateReceiver);
		}

		bool m_RegisteredForCallbacks = false;

		internal void RegisterForCallbacks()
		{
			if (CallbackHooksEnabled && !m_RegisteredForCallbacks)
			{
				m_RegisteredForCallbacks = true;
				MonoBehaviourCallbackHooks.Instance.OnUpdateDelegate += Update;
			}
		}

		/// <summary>
		/// Clears out the diagnostics callback handler.
		/// </summary>
		[Obsolete("ClearDiagnosticsCallback is Obsolete, use ClearDiagnosticCallbacks instead.")]
		public void ClearDiagnosticsCallback()
		{
			m_diagnosticsHandler = null;
			m_obsoleteDiagnosticsHandler = null;
		}

		/// <summary>
		/// Clears out the diagnostics callbacks handler.
		/// </summary>
		public void ClearDiagnosticCallbacks()
		{
			m_diagnosticsHandler = null;
			m_obsoleteDiagnosticsHandler = null;
		}

		/// <summary>
		/// Unregister a handler for diagnostic events.
		/// </summary>
		/// <param name="func">The event handler function.</param>
		public void UnregisterDiagnosticCallback(Action<DiagnosticEventContext> func)
		{
			if (m_diagnosticsHandler != null)
				m_diagnosticsHandler -= func;
			else
				Debug.LogError("No Diagnostic callbacks registered, cannot remove callback.");
		}

		/// <summary>
		/// Register a handler for diagnostic events.
		/// </summary>
		/// <param name="func">The event handler function.</param>
		[Obsolete]
		public void RegisterDiagnosticCallback(Action<AsyncOperationHandle, ResourceManager.DiagnosticEventType, int, object> func)
		{
			m_obsoleteDiagnosticsHandler = func;
		}

		/// <summary>
		/// Register a handler for diagnostic events.
		/// </summary>
		/// <param name="func">The event handler function.</param>
		public void RegisterDiagnosticCallback(Action<DiagnosticEventContext> func)
		{
			m_diagnosticsHandler += func;
		}

		internal void PostDiagnosticEvent(DiagnosticEventContext context)
		{
			m_diagnosticsHandler?.Invoke(context);

			if (m_obsoleteDiagnosticsHandler == null)
				return;
			m_obsoleteDiagnosticsHandler(context.OperationHandle, context.Type, context.EventValue, string.IsNullOrEmpty(context.Error) ? context.Context : context.Error);
		}

		/// <summary>
		/// Gets the appropriate <see cref="IResourceProvider"/> for the given <paramref name="location"/> and <paramref name="type"/>.
		/// </summary>
		/// <returns>The resource provider. Or null if an appropriate provider cannot be found</returns>
		/// <param name="t">The desired object type to be loaded from the provider.</param>
		/// <param name="location">The resource location.</param>
		public IResourceProvider GetResourceProvider(Type t, IResourceLocation location)
		{
			if (location != null)
			{
				IResourceProvider prov = null;
				var hash = location.ProviderId.GetHashCode() * 31 + (t == null ? 0 : t.GetHashCode());
				if (!m_providerMap.TryGetValue(hash, out prov))
				{
					for (int i = 0; i < ResourceProviders.Count; i++)
					{
						var p = ResourceProviders[i];
						if (p.ProviderId.Equals(location.ProviderId, StringComparison.Ordinal) && (t == null || p.CanProvide(t, location)))
						{
							m_providerMap.Add(hash, prov = p);
							break;
						}
					}
				}

				return prov;
			}

			return null;
		}

		Type GetDefaultTypeForLocation(IResourceLocation loc)
		{
			var provider = GetResourceProvider(null, loc);
			if (provider == null)
				return typeof(object);
			Type t = provider.GetDefaultType(loc);
			return t != null ? t : typeof(object);
		}

		private int CalculateLocationsHash(IList<IResourceLocation> locations, Type t = null)
		{
			if (locations == null || locations.Count == 0)
				return 0;
			int hash = 17;
			foreach (var loc in locations)
			{
				Type t2 = t != null ? t : GetDefaultTypeForLocation(loc);
				hash = hash * 31 + loc.Hash(t2);
			}

			return hash;
		}

		/// <summary>
		/// Load the <typeparamref name="TObject"/> at the specified <paramref name="location"/>.
		/// </summary>
		/// <returns>An async operation.</returns>
		/// <param name="location">Location to load.</param>
		/// <param name="releaseDependenciesOnFailure">When true, if the operation fails, dependencies will be released.</param>
		/// <typeparam name="TObject">Object type to load.</typeparam>
		private AsyncOperationHandle ProvideResource(IResourceLocation location, Type desiredType = null, bool releaseDependenciesOnFailure = true)
		{
			if (location == null)
				throw new ArgumentNullException("location");
			IResourceProvider provider = null;
			if (desiredType == null)
			{
				provider = GetResourceProvider(desiredType, location);
				if (provider == null)
				{
					var ex = new UnknownResourceProviderException(location);
					return CreateCompletedOperationInternal<object>(null, false, ex, releaseDependenciesOnFailure);
				}

				desiredType = provider.GetDefaultType(location);
			}

			if (provider == null)
				provider = GetResourceProvider(desiredType, location);

			IAsyncOperation op;
			IOperationCacheKey key = CreateCacheKeyForLocation(provider, location, desiredType);

			if (m_AssetOperationCache.TryGetValue(key, out op))
			{
				op.IncrementReferenceCount();
				return new AsyncOperationHandle(op, location.ToString());
			}

			Type provType;
			if (!m_ProviderOperationTypeCache.TryGetValue(desiredType, out provType))
				m_ProviderOperationTypeCache.Add(desiredType, provType = typeof(ProviderOperation<>).MakeGenericType(new Type[] { desiredType }));
			op = CreateOperation<IAsyncOperation>(provType, provType.GetHashCode(), key, m_ReleaseOpCached);

			// Calculate the hash of the dependencies
			int depHash = location.DependencyHashCode;
			var depOp = location.HasDependencies
				? ProvideResourceGroupCached(location.Dependencies, depHash, null, null, releaseDependenciesOnFailure)
				: default(AsyncOperationHandle<IList<AsyncOperationHandle>>);

			((IGenericProviderOperation)op).Init(this, provider, location, depOp, releaseDependenciesOnFailure);

			var handle = StartOperation(op, depOp);
			handle.LocationName = location.ToString();

			if (depOp.IsValid())
				depOp.Release();

			return handle;
		}

		internal IAsyncOperation GetOperationFromCache(IResourceLocation location, Type desiredType)
		{
			IResourceProvider provider = GetResourceProvider(desiredType, location);
			IOperationCacheKey key = CreateCacheKeyForLocation(provider, location, desiredType);

			if (m_AssetOperationCache.TryGetValue(key, out IAsyncOperation op))
				return op;
			return null;
		}

		/// <summary>
		/// Creates the appropriate cache key for a given location + provider.  AssetBundleProviders need to use a different
		/// cache key than regular locations.
		/// </summary>
		/// <param name="provider">The ResourceProvider for the given location</param>
		/// <param name="location">The location of the asset we're attempting to request</param>
		/// <param name="desiredType">The desired type of the asset we're attempting to request.</param>
		/// <returns>An IOperationCacheKey for use with the async operation cache.</returns>
		internal IOperationCacheKey CreateCacheKeyForLocation(IResourceProvider provider, IResourceLocation location, Type desiredType = null)
		{
			IOperationCacheKey key = null;
			bool isAssetBundleProvider = false;
			if (provider != null)
			{
                if (m_AssetBundleProviders.Contains(provider))
					isAssetBundleProvider = true;
				else if (typeof(AssetBundleProvider).IsAssignableFrom(provider.GetType()))
				{
					isAssetBundleProvider = true;
                    m_AssetBundleProviders.Add(provider);
				}
			}

            //Actual key generation has been moved to AssetBundleProvider virtual method to allow provider derived from AssetBundleProvider
            //skip calling TransformInternalId method, as it may return different values before and after asset bundle is loaded,
            //for example when using Play Asset Delivery for Android.
            //For major package release consider this method to be made a part of IResourceProvider to simplify this whole logic.
			if (isAssetBundleProvider)
                key = (provider as AssetBundleProvider).CreateCacheKeyForLocation(this, location, desiredType);
			else
				key = new LocationCacheKey(location, desiredType);

			return key;
		}

		Dictionary<Type, Type> m_ProviderOperationTypeCache = new Dictionary<Type, Type>();

		/// <summary>
		/// Load the <typeparamref name="TObject"/> at the specified <paramref name="location"/>.
		/// </summary>
		/// <returns>An async operation.</returns>
		/// <param name="location">Location to load.</param>
		/// <typeparam name="TObject">Object type to load.</typeparam>
		public AsyncOperationHandle<TObject> ProvideResource<TObject>(IResourceLocation location)
		{
			AsyncOperationHandle handle = ProvideResource(location, typeof(TObject));
			return handle.Convert<TObject>();
		}

		/// <summary>
		/// Registers an operation with the ResourceManager. The operation will be executed when the <paramref name="dependency"/> completes.
		/// This should only be used when creating custom operations.
		/// </summary>
		/// <returns>The AsyncOperationHandle used to access the result and status of the operation.</returns>
		/// <param name="operation">The custom AsyncOperationBase object</param>
		/// <param name="dependency">Execution of the operation will not occur until this handle completes. A default handle can be passed if no dependency is required.</param>
		/// <typeparam name="TObject">Object type associated with this operation.</typeparam>
		public AsyncOperationHandle<TObject> StartOperation<TObject>(AsyncOperationBase<TObject> operation, AsyncOperationHandle dependency)
		{
			operation.Start(this, dependency, m_UpdateCallbacks);
			return operation.Handle;
		}

		internal AsyncOperationHandle StartOperation(IAsyncOperation operation, AsyncOperationHandle dependency)
		{
			operation.Start(this, dependency, m_UpdateCallbacks);
			return operation.Handle;
		}

		class CompletedOperation<TObject> : AsyncOperationBase<TObject>
		{
			bool m_Success;
			Exception m_Exception;
			bool m_ReleaseDependenciesOnFailure;

			public CompletedOperation()
			{
			}

			public void Init(TObject result, bool success, string errorMsg, bool releaseDependenciesOnFailure = true)
			{
				Init(result, success, !string.IsNullOrEmpty(errorMsg) ? new Exception(errorMsg) : null, releaseDependenciesOnFailure);
			}

			public void Init(TObject result, bool success, Exception exception, bool releaseDependenciesOnFailure = true)
			{
				Result = result;
				m_Success = success;
				m_Exception = exception;
				m_ReleaseDependenciesOnFailure = releaseDependenciesOnFailure;
			}

			protected override string DebugName
			{
				get { return "CompletedOperation"; }
			}

			///<inheritdoc />
			protected override bool InvokeWaitForCompletion()
			{
				m_RM?.Update(Time.unscaledDeltaTime);
				if (!HasExecuted)
					InvokeExecute();
				return true;
			}

			protected override void Execute()
			{
				Complete(Result, m_Success, m_Exception, m_ReleaseDependenciesOnFailure);
			}
		}

		void OnInstanceOperationDestroy(IAsyncOperation o)
		{
			m_TrackedInstanceOperations.Remove(o as InstanceOperation);
			Allocator.Release(o.GetType().GetHashCode(), o);
		}

		void OnOperationDestroyNonCached(IAsyncOperation o)
		{
			Allocator.Release(o.GetType().GetHashCode(), o);
		}

		void OnOperationDestroyCached(IAsyncOperation o)
		{
			Allocator.Release(o.GetType().GetHashCode(), o);
			var cachable = o as ICachable;
			if (cachable?.Key != null)
            {
				RemoveOperationFromCache(cachable.Key);
                cachable.Key = null;
            }

		}

		internal T CreateOperation<T>(Type actualType, int typeHash, IOperationCacheKey cacheKey, Action<IAsyncOperation> onDestroyAction) where T : IAsyncOperation
		{
			if (cacheKey == null)
			{
				var op = (T)Allocator.New(actualType, typeHash);
				op.OnDestroy = onDestroyAction;
				return op;
			}
			else
			{
				var op = (T)Allocator.New(actualType, typeHash);
				op.OnDestroy = onDestroyAction;
				if (op is ICachable cachable)
				{
					cachable.Key = cacheKey;
					AddOperationToCache(cacheKey, op);
				}

				return op;
			}
		}

		internal void AddOperationToCache(IOperationCacheKey key, IAsyncOperation operation)
		{
			if (!IsOperationCached(key))
				m_AssetOperationCache.Add(key, operation);
		}

		internal bool RemoveOperationFromCache(IOperationCacheKey key)
		{
			if (!IsOperationCached(key))
				return true;

			return m_AssetOperationCache.Remove(key);
		}

		internal bool IsOperationCached(IOperationCacheKey key)
		{
			return m_AssetOperationCache.ContainsKey(key);
		}

		internal int CachedOperationCount()
		{
			return m_AssetOperationCache.Count;
		}

		/// <summary>
		/// Creates an operation that has already completed with a specified result and error message./>.
		/// </summary>
		/// <param name="result">The result that the operation will provide.</param>
		/// <param name="errorMsg">The error message if the operation should be in the failed state. Otherwise null or empty string.</param>
		/// <typeparam name="TObject">Object type.</typeparam>
		/// <returns>The operation handle used for the completed operation.</returns>
		public AsyncOperationHandle<TObject> CreateCompletedOperation<TObject>(TObject result, string errorMsg)
		{
			var success = string.IsNullOrEmpty(errorMsg);
			return CreateCompletedOperationInternal(result, success, !success ? new Exception(errorMsg) : null);
		}

		/// <summary>
		/// Creates an operation that has already completed with a specified result and error message./>.
		/// </summary>
		/// <param name="result">The result that the operation will provide.</param>
		/// <param name="exception">The exception with an error message if the operation should be in the failed state. Otherwise null.</param>
		/// <typeparam name="TObject">Object type.</typeparam>
		/// <returns>The operation handle used for the completed operation.</returns>
		public AsyncOperationHandle<TObject> CreateCompletedOperationWithException<TObject>(TObject result, Exception exception)
		{
			return CreateCompletedOperationInternal(result, exception == null, exception);
		}

		internal AsyncOperationHandle<TObject> CreateCompletedOperationInternal<TObject>(TObject result, bool success, Exception exception, bool releaseDependenciesOnFailure = true)
		{
			var cop = CreateOperation<CompletedOperation<TObject>>(typeof(CompletedOperation<TObject>), typeof(CompletedOperation<TObject>).GetHashCode(), null, m_ReleaseOpNonCached);
			cop.Init(result, success, exception, releaseDependenciesOnFailure);
			return StartOperation(cop, default(AsyncOperationHandle));
		}

		/// <summary>
		/// Release the operation associated with the specified handle
		/// </summary>
		/// <param name="handle">The handle to release.</param>
		public void Release(AsyncOperationHandle handle)
		{
			handle.Release();
		}

		/// <summary>
		/// Increment reference count of operation handle.
		/// </summary>
		/// <param name="handle">The handle to the resource to increment the reference count for.</param>
		public void Acquire(AsyncOperationHandle handle)
		{
			handle.Acquire();
		}

		private GroupOperation AcquireGroupOpFromCache(IOperationCacheKey key)
		{
			IAsyncOperation opGeneric;
			if (m_AssetOperationCache.TryGetValue(key, out opGeneric))
			{
				opGeneric.IncrementReferenceCount();
				return (GroupOperation)opGeneric;
			}

			return null;
		}

		/// <summary>
		/// Create a group operation for a set of locations.
		/// </summary>
		/// <typeparam name="T">The expected object type for the operations.</typeparam>
		/// <param name="locations">The list of locations to load.</param>
		/// <returns>The operation for the entire group.</returns>
		public AsyncOperationHandle<IList<AsyncOperationHandle>> CreateGroupOperation<T>(IList<IResourceLocation> locations)
		{
			var op = CreateOperation<GroupOperation>(typeof(GroupOperation), s_GroupOperationTypeHash, null, m_ReleaseOpNonCached);
			var ops = new List<AsyncOperationHandle>(locations.Count);
			foreach (var loc in locations)
				ops.Add(ProvideResource<T>(loc));

			op.Init(ops);
			return StartOperation(op, default);
		}

		/// <summary>
		/// Create a group operation for a set of locations.
		/// </summary>
		/// <typeparam name="T">The expected object type for the operations.</typeparam>
		/// <param name="locations">The list of locations to load.</param>
		/// <param name="allowFailedDependencies">The operation succeeds if any grouped locations fail.</param>
		/// <returns>The operation for the entire group.</returns>
		internal AsyncOperationHandle<IList<AsyncOperationHandle>> CreateGroupOperation<T>(IList<IResourceLocation> locations, bool allowFailedDependencies)
		{
			var op = CreateOperation<GroupOperation>(typeof(GroupOperation), s_GroupOperationTypeHash, null, m_ReleaseOpNonCached);
			var ops = new List<AsyncOperationHandle>(locations.Count);
			foreach (var loc in locations)
				ops.Add(ProvideResource<T>(loc));

			GroupOperation.GroupOperationSettings settings = GroupOperation.GroupOperationSettings.None;
			if (allowFailedDependencies)
				settings |= GroupOperation.GroupOperationSettings.AllowFailedDependencies;
			op.Init(ops, settings);
			return StartOperation(op, default);
		}

		/// <summary>
		/// Create a group operation for a set of AsyncOperationHandles
		/// </summary>
		/// <param name="operations">The list of operations that need to complete.</param>
		/// <param name="releasedCachedOpOnComplete">Determine if the cached operation should be released or not.</param>
		/// <returns>The operation for the entire group</returns>
		public AsyncOperationHandle<IList<AsyncOperationHandle>> CreateGenericGroupOperation(List<AsyncOperationHandle> operations, bool releasedCachedOpOnComplete = false)
		{
			var op = CreateOperation<GroupOperation>(typeof(GroupOperation), s_GroupOperationTypeHash, new AsyncOpHandlesCacheKey(operations),
				releasedCachedOpOnComplete ? m_ReleaseOpCached : m_ReleaseOpNonCached);
			op.Init(operations);
			return StartOperation(op, default);
		}

		internal AsyncOperationHandle<IList<AsyncOperationHandle>> ProvideResourceGroupCached(
			IList<IResourceLocation> locations, int groupHash, Type desiredType, Action<AsyncOperationHandle> callback, bool releaseDependenciesOnFailure = true)
		{
			var depsKey = new DependenciesCacheKey(locations, groupHash);
			GroupOperation op = AcquireGroupOpFromCache(depsKey);
			AsyncOperationHandle<IList<AsyncOperationHandle>> handle;
			if (op == null)
			{
				op = CreateOperation<GroupOperation>(typeof(GroupOperation), s_GroupOperationTypeHash, depsKey, m_ReleaseOpCached);
				var ops = new List<AsyncOperationHandle>(locations.Count);
				foreach (var loc in locations)
					ops.Add(ProvideResource(loc, desiredType, releaseDependenciesOnFailure));

				op.Init(ops, releaseDependenciesOnFailure);

				handle = StartOperation(op, default(AsyncOperationHandle));
			}
			else
			{
				handle = op.Handle;
			}

			if (callback != null)
			{
				var depOps = op.GetDependentOps();
				for (int i = 0; i < depOps.Count; i++)
				{
					depOps[i].Completed += callback;
				}
			}

			return handle;
		}

		/// <summary>
		/// Asynchronously load all objects in the given collection of <paramref name="locations"/>.
		/// If any matching location fails, all loads and dependencies will be released.  The returned .Result will be null, and .Status will be Failed.
		/// </summary>
		/// <returns>An async operation that will complete when all individual async load operations are complete.</returns>
		/// <param name="locations">locations to load.</param>
		/// <param name="callback">This callback will be invoked once for each object that is loaded.</param>
		/// <typeparam name="TObject">Object type to load.</typeparam>
		public AsyncOperationHandle<IList<TObject>> ProvideResources<TObject>(IList<IResourceLocation> locations, Action<TObject> callback = null)
		{
			return ProvideResources(locations, true, callback);
		}

		/// <summary>
		/// Asynchronously load all objects in the given collection of <paramref name="locations"/>.
		/// </summary>
		/// <returns>An async operation that will complete when all individual async load operations are complete.</returns>
		/// <param name="locations">locations to load.</param>
		/// <param name="releaseDependenciesOnFailure">
		/// If all matching locations succeed, this parameter is ignored.
		/// When true, if any matching location fails, all loads and dependencies will be released.  The returned .Result will be null, and .Status will be Failed.
		/// When false, if any matching location fails, the returned .Result will be an IList of size equal to the number of locations attempted.  Any failed location will
		/// correlate to a null in the IList, while successful loads will correlate to a TObject in the list. The .Status will still be Failed.
		/// When true, op does not need to be released if anything fails, when false, it must always be released.
		/// </param>
		/// <param name="callback">This callback will be invoked once for each object that is loaded.</param>
		/// <typeparam name="TObject">Object type to load.</typeparam>
		public AsyncOperationHandle<IList<TObject>> ProvideResources<TObject>(IList<IResourceLocation> locations, bool releaseDependenciesOnFailure, Action<TObject> callback = null)
		{
			if (locations == null)
				return CreateCompletedOperation<IList<TObject>>(null, "Null Location");

			Action<AsyncOperationHandle> callbackGeneric = null;
			if (callback != null)
			{
				callbackGeneric = (x) => callback((TObject)(x.Result));
			}

			var typelessHandle = ProvideResourceGroupCached(locations, CalculateLocationsHash(locations, typeof(TObject)), typeof(TObject), callbackGeneric, releaseDependenciesOnFailure);
			var chainOp = CreateChainOperation<IList<TObject>>(typelessHandle, (resultHandle) =>
			{
				AsyncOperationHandle<IList<AsyncOperationHandle>> handleToHandles = resultHandle.Convert<IList<AsyncOperationHandle>>();

				var list = new List<TObject>();
				Exception exception = null;
				if (handleToHandles.Status == AsyncOperationStatus.Succeeded)
				{
					foreach (var r in handleToHandles.Result)
						list.Add(r.Convert<TObject>().Result);
				}
				else
				{
					bool foundSuccess = false;
					if (!releaseDependenciesOnFailure)
					{
						foreach (AsyncOperationHandle handle in handleToHandles.Result)
						{
							if (handle.Status == AsyncOperationStatus.Succeeded)
							{
								list.Add(handle.Convert<TObject>().Result);
								foundSuccess = true;
							}
							else
								list.Add(default(TObject));
						}
					}

					if (!foundSuccess)
					{
						list = null;
						exception = new ResourceManagerException("ProvideResources failed", handleToHandles.OperationException);
					}
					else
					{
						exception = new ResourceManagerException("Partial success in ProvideResources.  Some items failed to load. See earlier logs for more info.",
							handleToHandles.OperationException);
					}
				}

				return CreateCompletedOperationInternal<IList<TObject>>(list, exception == null, exception, releaseDependenciesOnFailure);
			}, releaseDependenciesOnFailure);

			// chain operation holds the dependency
			typelessHandle.Release();
			return chainOp;
		}

		/// <summary>
		/// Create a chain operation to handle dependencies.
		/// </summary>
		/// <typeparam name="TObject">The type of operation handle to return.</typeparam>
		/// <typeparam name="TObjectDependency">The type of the dependency operation.</typeparam>
		/// <param name="dependentOp">The dependency operation.</param>
		/// <param name="callback">The callback method that will create the dependent operation from the dependency operation.</param>
		/// <returns>The operation handle.</returns>
		public AsyncOperationHandle<TObject> CreateChainOperation<TObject, TObjectDependency>(AsyncOperationHandle<TObjectDependency> dependentOp,
			Func<AsyncOperationHandle<TObjectDependency>, AsyncOperationHandle<TObject>> callback)
		{
			var op = CreateOperation<ChainOperation<TObject, TObjectDependency>>(typeof(ChainOperation<TObject, TObjectDependency>), typeof(ChainOperation<TObject, TObjectDependency>).GetHashCode(),
				null, null);
			op.Init(dependentOp, callback, true);
			return StartOperation(op, dependentOp);
		}

		/// <summary>
		/// Create a chain operation to handle dependencies.
		/// </summary>
		/// <typeparam name="TObject">The type of operation handle to return.</typeparam>
		/// <param name="dependentOp">The dependency operation.</param>
		/// <param name="callback">The callback method that will create the dependent operation from the dependency operation.</param>
		/// <returns>The operation handle.</returns>
		public AsyncOperationHandle<TObject> CreateChainOperation<TObject>(AsyncOperationHandle dependentOp, Func<AsyncOperationHandle, AsyncOperationHandle<TObject>> callback)
		{
			var cOp = new ChainOperationTypelessDependency<TObject>();
			cOp.Init(dependentOp, callback, true);
			return StartOperation(cOp, dependentOp);
		}

		/// <summary>
		/// Create a chain operation to handle dependencies.
		/// </summary>
		/// <typeparam name="TObject">The type of operation handle to return.</typeparam>
		/// <typeparam name="TObjectDependency">The type of the dependency operation.</typeparam>
		/// <param name="dependentOp">The dependency operation.</param>
		/// <param name="callback">The callback method that will create the dependent operation from the dependency operation.</param>
		/// <param name="releaseDependenciesOnFailure"> Whether to release dependencies if the created operation has failed.</param>
		/// <returns>The operation handle.</returns>
		public AsyncOperationHandle<TObject> CreateChainOperation<TObject, TObjectDependency>(AsyncOperationHandle<TObjectDependency> dependentOp,
			Func<AsyncOperationHandle<TObjectDependency>, AsyncOperationHandle<TObject>> callback, bool releaseDependenciesOnFailure = true)
		{
			var op = CreateOperation<ChainOperation<TObject, TObjectDependency>>(typeof(ChainOperation<TObject, TObjectDependency>), typeof(ChainOperation<TObject, TObjectDependency>).GetHashCode(),
				null, null);
			op.Init(dependentOp, callback, releaseDependenciesOnFailure);
			return StartOperation(op, dependentOp);
		}

		/// <summary>
		/// Create a chain operation to handle dependencies.
		/// </summary>
		/// <typeparam name="TObject">The type of operation handle to return.</typeparam>
		/// <param name="dependentOp">The dependency operation.</param>
		/// <param name="callback">The callback method that will create the dependent operation from the dependency operation.</param>
		/// <param name="releaseDependenciesOnFailure"> Whether to release dependencies if the created operation has failed.</param>
		/// <returns>The operation handle.</returns>
		public AsyncOperationHandle<TObject> CreateChainOperation<TObject>(AsyncOperationHandle dependentOp, Func<AsyncOperationHandle, AsyncOperationHandle<TObject>> callback,
			bool releaseDependenciesOnFailure = true)
		{
			var cOp = new ChainOperationTypelessDependency<TObject>();
			cOp.Init(dependentOp, callback, releaseDependenciesOnFailure);
			return StartOperation(cOp, dependentOp);
		}

		internal class InstanceOperation : AsyncOperationBase<GameObject>
		{
			AsyncOperationHandle<GameObject> m_dependency;
			InstantiationParameters m_instantiationParams;
			IInstanceProvider m_instanceProvider;
			GameObject m_instance;
			Scene m_scene;

			public void Init(ResourceManager rm, IInstanceProvider instanceProvider, InstantiationParameters instantiationParams, AsyncOperationHandle<GameObject> dependency)
			{
				m_RM = rm;
				m_dependency = dependency;
				m_instanceProvider = instanceProvider;
				m_instantiationParams = instantiationParams;
				m_scene = default(Scene);
			}

			internal override DownloadStatus GetDownloadStatus(HashSet<object> visited)
			{
				return m_dependency.IsValid() ? m_dependency.InternalGetDownloadStatus(visited) : new DownloadStatus() { IsDone = IsDone };
			}

			/// <inheritdoc />
			public override void GetDependencies(List<AsyncOperationHandle> deps)
			{
				deps.Add(m_dependency);
			}

			protected override string DebugName
			{
				get
				{
					if (m_instanceProvider == null)
						return "Instance<Invalid>";
					return string.Format("Instance<{0}>({1}", m_instanceProvider.GetType().Name, m_dependency.IsValid() ? m_dependency.DebugName : "Invalid");
				}
			}

			public Scene InstanceScene() => m_scene;

			protected override void Destroy()
			{
				m_instanceProvider.ReleaseInstance(m_RM, m_instance);
			}

			protected override float Progress
			{
				get { return m_dependency.PercentComplete; }
			}

			///<inheritdoc />
			protected override bool InvokeWaitForCompletion()
			{
				if (m_dependency.IsValid() && !m_dependency.IsDone)
					m_dependency.WaitForCompletion();

				m_RM?.Update(Time.unscaledDeltaTime);
				if (m_instance == null && !HasExecuted)
					InvokeExecute();

				return IsDone;
			}

			protected override void Execute()
			{
				Exception e = m_dependency.OperationException;
				if (m_dependency.Status == AsyncOperationStatus.Succeeded)
				{
					m_instance = m_instanceProvider.ProvideInstance(m_RM, m_dependency, m_instantiationParams);
					if (m_instance != null)
						m_scene = m_instance.scene;
					Complete(m_instance, true, null);
				}
				else
				{
					Complete(m_instance, false, string.Format("Dependency operation failed with {0}.", e));
				}
			}
		}

		/// <summary>
		/// Load a scene at a specified resource location.
		/// </summary>
		/// <param name="sceneProvider">The scene provider instance.</param>
		/// <param name="location">The location of the scene.</param>
		/// <param name="loadSceneMode">The load mode for the scene.</param>
		/// <param name="activateOnLoad">If false, the scene will be loaded in the background and not activated when complete.</param>
		/// <param name="priority">The priority for the load operation.</param>
		/// <returns>Async operation handle that will complete when the scene is loaded.  If activateOnLoad is false, then Activate() will need to be called on the SceneInstance returned.</returns>
		public AsyncOperationHandle<SceneInstance> ProvideScene(ISceneProvider sceneProvider, IResourceLocation location, LoadSceneMode loadSceneMode, bool activateOnLoad, int priority)
		{
			if (sceneProvider == null)
				throw new NullReferenceException("sceneProvider is null");

			return sceneProvider.ProvideScene(this, location, new LoadSceneParameters(loadSceneMode), activateOnLoad, priority);
		}

		/// <summary>
		/// Load a scene at a specified resource location.
		/// </summary>
		/// <param name="sceneProvider">The scene provider instance.</param>
		/// <param name="location">The location of the scene.</param>
		/// <param name="loadSceneParameters">The load parameters for the scene.</param>
		/// <param name="activateOnLoad">If false, the scene will be loaded in the background and not activated when complete.</param>
		/// <param name="priority">The priority for the load operation.</param>
		/// <returns>Async operation handle that will complete when the scene is loaded.  If activateOnLoad is false, then Activate() will need to be called on the SceneInstance returned.</returns>
		public AsyncOperationHandle<SceneInstance> ProvideScene(ISceneProvider sceneProvider, IResourceLocation location, LoadSceneParameters loadSceneParameters, bool activateOnLoad, int priority)
		{
			if (sceneProvider == null)
				throw new NullReferenceException("sceneProvider is null");

			return sceneProvider.ProvideScene(this, location, loadSceneParameters, activateOnLoad, priority);
		}

		/// <summary>
		/// Release a scene.
		/// </summary>
		/// <param name="sceneProvider">The scene provider.</param>
		/// <param name="sceneLoadHandle">The operation handle used to load the scene.</param>
		/// <returns>An operation handle for the unload.</returns>
		public AsyncOperationHandle<SceneInstance> ReleaseScene(ISceneProvider sceneProvider, AsyncOperationHandle<SceneInstance> sceneLoadHandle)
		{
			if (sceneProvider == null)
				throw new NullReferenceException("sceneProvider is null");
			//           if (sceneLoadHandle.ReferenceCount == 0)
			//               return CreateCompletedOperation<SceneInstance>(default(SceneInstance), "");
			return sceneProvider.ReleaseScene(this, sceneLoadHandle);
		}

		/// <summary>
		/// Asynchronously instantiate a prefab (GameObject) at the specified <paramref name="location"/>.
		/// </summary>
		/// <returns>Async operation that will complete when the prefab is instantiated.</returns>
		/// <param name="provider">An implementation of IInstanceProvider that will be used to instantiate and destroy the GameObject.</param>
		/// <param name="location">Location of the prefab.</param>
		/// <param name="instantiateParameters">A struct containing the parameters to pass the the Instantiation call.</param>
		public AsyncOperationHandle<GameObject> ProvideInstance(IInstanceProvider provider, IResourceLocation location, InstantiationParameters instantiateParameters)
		{
			if (provider == null)
				throw new NullReferenceException("provider is null.  Assign a valid IInstanceProvider object before using.");

			if (location == null)
				throw new ArgumentNullException("location");

			var depOp = ProvideResource<GameObject>(location);
			var baseOp = CreateOperation<InstanceOperation>(typeof(InstanceOperation), s_InstanceOperationTypeHash, null, m_ReleaseInstanceOp);
			baseOp.Init(this, provider, instantiateParameters, depOp);
			m_TrackedInstanceOperations.Add(baseOp);
			return StartOperation<GameObject>(baseOp, depOp);
		}

		/// <summary>
		/// Releases all instances of the given scene.
		/// </summary>
		/// <param name="scene">The scene whose instances should be released.</param>
		public void CleanupSceneInstances(Scene scene)
		{
			List<InstanceOperation> handlesToRelease = null;
			foreach (var h in m_TrackedInstanceOperations)
			{
				if (h.Result == null && scene == h.InstanceScene())
				{
					if (handlesToRelease == null)
						handlesToRelease = new List<InstanceOperation>();
					handlesToRelease.Add(h);
				}
			}

			if (handlesToRelease != null)
			{
				foreach (var h in handlesToRelease)
				{
					m_TrackedInstanceOperations.Remove(h);
					h.DecrementReferenceCount();
				}
			}
		}

		private void ExecuteDeferredCallbacks()
		{
			m_InsideExecuteDeferredCallbacksMethod = true;
			for (int i = 0; i < m_DeferredCompleteCallbacks.Count; i++)
			{
				m_DeferredCompleteCallbacks[i].InvokeCompletionEvent();
				m_DeferredCompleteCallbacks[i].DecrementReferenceCount();
			}

			m_DeferredCompleteCallbacks.Clear();
			m_InsideExecuteDeferredCallbacksMethod = false;
		}

		internal void RegisterForDeferredCallback(IAsyncOperation op, bool incrementRefCount = true)
		{
			if (CallbackHooksEnabled && m_InsideExecuteDeferredCallbacksMethod)
			{
				if (m_DeferredCallbacksToRegister == null)
					m_DeferredCallbacksToRegister = new List<DeferredCallbackRegisterRequest>();
				m_DeferredCallbacksToRegister.Add
				(
					new DeferredCallbackRegisterRequest()
					{
						operation = op,
						incrementRefCount = incrementRefCount
					}
				);
			}
			else
			{
				if (incrementRefCount)
					op.IncrementReferenceCount();
				m_DeferredCompleteCallbacks.Add(op);
				RegisterForCallbacks();
			}
		}

		internal void Update(float unscaledDeltaTime)
		{
			if (m_InsideUpdateMethod)
				throw new Exception("Reentering the Update method is not allowed.  This can happen when calling WaitForCompletion on an operation while inside of a callback.");
			m_InsideUpdateMethod = true;
			m_UpdateCallbacks.Invoke(unscaledDeltaTime);
			m_UpdatingReceivers = true;
			for (int i = 0; i < m_UpdateReceivers.Count; i++)
				m_UpdateReceivers[i].Update(unscaledDeltaTime);
			m_UpdatingReceivers = false;
			if (m_UpdateReceiversToRemove != null)
			{
				foreach (var r in m_UpdateReceiversToRemove)
					m_UpdateReceivers.Remove(r);
				m_UpdateReceiversToRemove = null;
			}

			if (m_DeferredCallbacksToRegister != null)
			{
				foreach (DeferredCallbackRegisterRequest callback in m_DeferredCallbacksToRegister)
					RegisterForDeferredCallback(callback.operation, callback.incrementRefCount);
				m_DeferredCallbacksToRegister = null;
			}

			ExecuteDeferredCallbacks();
			m_InsideUpdateMethod = false;
		}

		/// <summary>
		/// Disposes internal resources used by the resource manager
		/// </summary>
		public void Dispose()
		{
			if (MonoBehaviourCallbackHooks.Exists && m_RegisteredForCallbacks)
			{
				MonoBehaviourCallbackHooks.Instance.OnUpdateDelegate -= Update;
				m_RegisteredForCallbacks = false;
			}
		}
	}
}
